const env = 'produce'


class Router {
    constructor() {
        this.routes = []
        this.initProxy()
    }

    initProxy() {
        self.addEventListener('fetch', event => {
            for (let route of this.routes) {
                if (match(route.rule, event.request)) {
                    respond(event, route.handler)
                    break
                }
            }
        })
    }

    registerRoute(rule, handler) {
        this.routes.push({ rule: rule, handler: handler })
    }
}

function respond(event, handler) {
    try {
        // 执行响应处理方法，根据返回结果进行兜底
        let res = handler(event.request)
        // 异步的响应结果兜底
        if (res instanceof Promise) {
            let promise = res.then(response => {
                // 如果返回结果非 Response 对象，抛出错误
                if (!(response instanceof Response)) {
                    throw Error('返回结果异常')
                }
                return response
            })
                // 异步响应错误处理，即直接返回状态码为 500 Response 对象
                .catch(() => new Response('Service Worker 出错', { status: 500 }))

            event.respondWith(promise)
            return
        }

        // 同步响应如果出现任何错误
        // 可以选择不调用 event.respondWith(r)
        // 让资源请求继续走浏览器默认的请求流程

        if (res instanceof Response) {
            event.respondWith(res)
        }
    } catch (e) { }
}


function match(rule, request) {
    switch (Object.prototype.toString.call(rule)) {
        // url 文本匹配
        case '[object String]':
            return request.url === new URL(rule, location).href

        // url 正则匹配
        case '[object RegExp]':
            if (env == 'develop' && request.url.match(rule)){
                console.log("匹配到url", request.url)
            }
            return request.url.match(rule)

        // url 传入函数自定义匹配
        case '[object Function]':
            return rule(request)
    }
}


// 以上是 Router 部分


class DB {
    constructor({
        dbName = 'db',
        version = 1,
        storeName
    }) {
        this.dbName = dbName
        this.storeName = storeName
        this.version = version
    }

    async getDB() {
        // 优先返回缓存的数据库示例
        if (this.db) {
            return this.db
        }
        // 打开数据库
        let request = indexedDB.open(this.dbName, this.version)
        // 当数据库初始化或升级时创建仓库
        request.onupgradeneeded = (event) => {
            let db = event.target.result
            // 仓库不存在时创建仓库，同时规定 key 为索引
            if (!db.objectStoreNames.contains(this.storeName)) {
                db.createObjectStore(this.storeName, { keyPath: 'key' })
            }
        }
        let event = await promisify(request)
        this.db = event.target.result
        return this.db
    }

    async setItem(key, value) {
        // 获取数据库
        let db = await this.getDB()
        // 创建事务，指定使用到的仓库名以及读写权限
        let transaction = db.transaction([this.storeName], 'readwrite')
        // 获取仓库实例
        let objectStore = transaction.objectStore(this.storeName)
        // 将 key & value 把装成对象 {key, value} 并存入仓库
        let request = objectStore.put({ key, value })
        // 异步执行结果通过 Promise 返回
        return promisify(request)
    }

    async getItem(key) {
        // 获取数据库实例
        let db = await this.getDB()
        // 创建事务，并制定好仓库名以及操作的只读权限
        let transaction = db.transaction([this.storeName], 'readonly')
        // 获取仓库示例
        let objectStore = transaction.objectStore(this.storeName)
        // 查找对应的数据并通过 Promise 返回对象包装后返回
        let request = objectStore.get(key)
        let event = await promisify(request)
        return event.target.result && event.target.result.value
    }

    async getAll() {
        let db = await this.getDB()
        let transaction = db.transaction([this.storeName], 'readonly')
        // 获取仓库实例
        let objectStore = transaction.objectStore(this.storeName)
        // 读取仓库的全部数据
        let request = objectStore.getAll()
        let event = await promisify(request)
        let result = event.target.result
        // 当数据为空时，返回空
        if (!result || !result.length) {
            return
        }
        // 数据不为空时，将数据包装成 Map对象并返回
        let map = new Map()
        for (let { key, value } of result) {
            map.set(key, value)
        }
        return map
    }

    async removeItem(key) {
        let db = await this.getDB()
        // 创建事务，指定仓库名以及删除操作的读写权限
        let transaction = db.transaction([this.storeName], 'readwrite')
        let objectStore = transaction.objectStore(this.storeName)
        // 删除数据
        let request = objectStore.delete(key)
        return promisify(request)
    }

}


function promisify(request) {
    return new Promise((resolve, reject) => {
        request.onsuccess = resolve
        request.onerror = reject
    })
}

class CacheWrapper{
    constructor({
        cacheName,
        expireOptions:{
            maxAgeSeconds
        }
    }){
        this.cacheName = cacheName
        this.maxAgeSeconds = maxAgeSeconds
    }

    async getCache(){
        if(!this.cache){
            // 如果没有 cache 则初始化之
            this.cache = await caches.open(this.cacheName)
        }
        return this.cache
    }

    getDB(){
        if(!this.db){
            // 如果没有 indexDB 则初始化之
            this.db = new DB({storeName: this.cacheName})
        }
        return this.db
    }

    async set (request, response){
        // 获取 db 对象
        let db = this.getDB()
        // 获取 cache 对象
        let cache = await this.getCache()
        // 同时更新本地缓存与资源所对应的过期时间
        await Promise.all([
            cache.put(request, response),
            db.setItem(request.url, Date.now() + this.maxAgeSeconds * 1000)
        ])
        // 清理过期资源
        this.deleteExprires()
    }

    async get (request) {
        let db = this.getDB()
        let cache = await this.getCache()
        let [response, expireTime] = await Promise.all([
            cache.match(request),
            db.getItem(request.url)
        ])
        if(expireTime > Date.now()){
            return response
        }
        this.deleteExprires()
    }

    async deleteExprires(){
        let db = this.getDB()
        let cache = await this.getCache()
        let map = await db.getAll()
        if(!map){
            return
        }
        let now = Date.now()
        for (let [url, expireTime] of map){
            if (expireTime <= now){
                await cache.delete(url)
            }
        }
    }
}

// 以上是 CacheWrapper 部分

// 以下是请求响应策略部分

// 公用方法

/* 向 cache 中写入资源 */
const cacheResponse = async (request, response)=>{
    let cache = await caches.open(cacheName)
    await cache.put(request, response)
}

/* 从 cache 中查找并返回资源 */
const getCachedResponse = async request => {
    let cache = await caches.open(cacheName)
    return cache.match(request, matchOptions)
}


/* 发起网络请求(复制原请求), 并且吧成功响应的对象存入cache */ 
const fetchAndCatch = async request => {

    const cacheResponse = async (request, response) => {
        // 使用 cacheName 参数打开缓存
        let cache = await caches.open(cacheName)
        await cache.put(request, response)
    }

    let response = await fetch(request.clone(), fetchOptions)
    // 请求资源失败时直接返回
    // if(!response.ok){
    //     return
    // }
    // 请求成功后, 将响应结果复制一份存入缓存
    // 空 catch用来兜底
    cacheResponse(request, response.clone())
        .catch(()=>{})
    return response
}


/**
 * 策略函数格式
 *
 * @param  {Request} request 当前请求对象
 * @return {Response|Promise.<Response>} 响应对象
 */

/* 网络优先策略 */
function networkFirst ({
    fetchOptions,
    cacheName = 'runtime-cache',
    matchOptions,
    onlyCopyURL = true
} = {}){

    /* 从 cache 中查找并返回资源 */
    const getCachedResponse = async request => {
        let cache = await caches.open(cacheName)
        return cache.match(request, matchOptions)
    }

    /* 向 cache 中写入资源 */
    const cacheResponse = async (request, response)=>{
        let cache = await caches.open(cacheName)
        await cache.put(request, response)
    }

    /* 发起网络请求, 并且把成功响应的对象存入cache */ 
    const fetchAndCatch = async request => {
        let response = await fetch(onlyCopyURL? new Request(request.url): request.clone()
            , fetchOptions)

        // 请求资源失败时直接返回
        // if(!response.ok){
        //     return
        // }
        // 请求成功后, 将响应结果复制一份存入缓存
        // 空 catch用来兜底
        cacheResponse(request, response.clone())
            .catch(()=>{
                if(env == 'develop'){
                    console.log("采用networkFirst时存入缓存失败")
                }
            })
        return response
    }

    return async request => {
        if(env == 'develop'){
            console.log('将netWorkFirst策略作用到请求', request)
        }
        let response
        try {
            response = await fetchAndCatch(request)
        } catch (e) {
            if(env =='develop'){
                console.log("网络请求错误, 尝试从缓存中取出响应")
            }
        }
        if(response == null){
            response = await getCachedResponse(request)
        }

        return response
    }
}

/* 缓存优先策略 */
function cacheFirst ({
    fetchOptions,
    cacheName = 'runtime-cache',
    matchOptions,
    onlyCopyURL = true
} = {}){

    /* 从 cache 中查找并返回资源 */
    const getCachedResponse = async request => {
        let cache = await caches.open(cacheName)
        return cache.match(request, matchOptions)
    }

    /* 将 request & response 存入缓存 */
    const cacheResponse = async (request, response) => {
        // 使用 cacheName 参数打开缓存
        let cache = await caches.open(cacheName)
        await cache.put(request, response)
    }

    /* 发起网络请求, 并且把成功响应的对象存入cache */ 
    const fetchAndCatch = async request => {
        let response = await fetch(onlyCopyURL? new Request(request.url): request.clone()
            , fetchOptions)

        // 请求资源失败时直接返回
        // if(!response.ok){
        //     return
        // }
        // 请求成功后, 将响应结果复制一份存入缓存
        // 空 catch用来兜底
        cacheResponse(request, response.clone())
            .catch(()=>{
                // debug
                if(env == 'develop'){
                    console.log("采用cacheFirst时试图存入缓存失败")
                }
            })
        return response
    }

    return async request => {

        // debug
        if (env == 'develop'){
            console.log('cacheFirst策略作用到请求', request)
        }
        
        let response
        try {
            response = await getCachedResponse(request)
        } catch (e) {}
        
        if(response == null){
            // debug
            if (env == 'develop'){
                console.log('从cache中获取失败, 尝试从网络获取')
            }
            response = await fetchAndCatch(request)
        }
        return response
    }
}

function networkOnly({
    fetchOptions
} = {}){
    return request => fetch(request, fetchOptions)
}

function cacheOnly({
    cacheName,
    matchOptions
} = {}){
    /* 从 cache 中查找并返回资源 */
    const getCachedResponse = async request => {
        let cache = await caches.open(cacheName)
        return cache.match(request, matchOptions)
    }

    return async request => {
        let response = await getCachedResponse(request)
        return response
    }
}

/* 优先返回本地缓存资源，并发送网络请求更新本地缓存 */
function staleWhileRevalidate({
    fetchOptions,
    cacheName = 'runtime-cache',
    matchOptions,
    onlyCopyURL = true
} = {}){
    

    /* 从 cache 中查找并返回资源 */
    const getCachedResponse = async request => {
        let cache = await caches.open(cacheName)
        return cache.match(request, matchOptions)
    }
    
    /* 向 cache 中写入资源 */
    const cacheResponse = async (request, response)=>{
        let cache = await caches.open(cacheName)
        await cache.put(request, response)
    }

    /* 发起网络请求, 并且把成功响应的对象存入cache */ 
    const fetchAndCatch = async request => {
        let response

        response = await fetch(onlyCopyURL? new Request(request.url): request.clone()
            , fetchOptions)

        // 请求资源失败时直接返回
        // if(!response.ok){
        //     return
        // }
        // 请求成功后, 将响应结果复制一份存入缓存
        // 空 catch用来兜底
        cacheResponse(request, response.clone())
            .catch(()=>{
                // debug
                if(env == 'develop'){
                    console.log("采用staleWhileRevalidate时试图存入缓存失败")
                }
            })
        return response
    }

    return async request => {

        // debug
        if (env == 'develop'){
            console.log('staleWhileRevalida te策略作用到请求', request)
        }

        let response
        // 尝试读取本地缓存 catch兜底
        try {
            response = await getCachedResponse(request)
        } catch (e) {
            if (env == 'develop'){
                console.log("staleWhileRevalidate获取缓存失败, 正在尝试网络更新")
            }
        }
        // 读取网络内容并更新缓存
        let fetchPromise = fetchAndCatch(request)
        // 如果本地有相应缓存，静默更新即可， 无需阻塞函数执行
        if (response){
            // 静默更新，无需报错
            fetchPromise.catch(e => {})
        }else{
            // 反之将网络请求到的资源返回
            response = await fetchPromise
        }
        return response

    }

}

// 以上是请求响应策略部分

addListenerInitStatus()

const router = new Router()

const cacheWrapper = new CacheWrapper({
    cacheName: 'image-cache',
    expireOptions:{
        maxAgeSeconds: 7*24*3600
    }
})

// 图片 cache
router.registerRoute(/\.(jpe?g|png|webp|JPE?G|PNG|WEBP)/, cacheFirst({cacheName: 'image-cache', onlyCopyURL: false}))

// 字体 cache
router.registerRoute(/\.(woff2)/, cacheFirst({cacheName: 'font-cache', onlyCopyURL: false}))

// cdn cache
router.registerRoute(/cdn\./, cacheFirst({cacheName: 'cdn-cache', onlyCopyURL: false}))

// 非 cdn的 css与 javascript文件
router.registerRoute(/(?!cdn).+\.(js|css)$/, staleWhileRevalidate({cacheName: 'js-css-cache', onlyCopyURL: false}))

// 主要页面的 html
var pageRegex = /^https?:\/\/reina\.link\/?[^\/]*\/?$/i
if(env == 'develop'){
    pageRegex = /^http:\/\/localhost:3000\/?[^\/]*\/?$/i
}
router.registerRoute(pageRegex, staleWhileRevalidate({cacheName: 'page-cache', onlyCopyURL: false}))


// router.registerRoute(/\.(jpe?g|png|webp|JPE?G|PNG|WEBP|css|js|woff2)/, async request => {
//     let response = await cacheWrapper.get(request)
//     // 如果缓存里有，就从缓存中拿
//     if(response){
//         return response
//     }
//     // 如果未缓存/缓存过期，发起网络请求获得最新图片资源
//     response = await fetch(request.url)
//     if(response.ok){
//         //console.log("缓存+1")
//         cacheWrapper.set(request, response.clone())
//     }
//     return response
// })

router.initProxy()




function addListenerInitStatus(){
    console.log("service worker 注册/解析成功")

    self.addEventListener('install', () => {
        console.log("service worker 安装成功")
    })

    self.addEventListener('activate', () => {
        console.log("service worker 激活成功")
    })
}